package com.minivision.fdiot.service;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import com.minivision.fdiot.entity.FaceSet;
import com.minivision.fdiot.rest.param.detect.SearchsParam;
import com.minivision.fdiot.rest.result.detect.*;
import org.apache.thrift.TException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import com.minivision.fastdfs.FdfsService;
import com.minivision.fdiot.entity.Face;
import com.minivision.fdiot.exception.ErrorType;
import com.minivision.fdiot.exception.FacePlatException;
import com.minivision.fdiot.rest.param.detect.SearchParam;
import com.minivision.fdiot.rest.result.detect.DetectedFace.Rectangle;
import com.minivision.fdiot.rest.result.detect.SearchResult.Result;
import com.minivision.fdiot.thrift.FaceFeatures;
import com.minivision.fdiot.thrift.FaceInfo;
import com.minivision.fdiot.thrift.ImageData;
import com.minivision.fdiot.thrift.Serv.Iface;
import com.minivision.fdiot.thrift.pool.ThriftServiceClientProxyFactory;
import com.minivision.fdiot.util.FeatureUtils;
import com.minivision.fdiot.util.ImageUtils;

import lombok.extern.slf4j.Slf4j;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

@Service
@Slf4j
@ConfigurationProperties("algorithm.param")
public class FaceServiceThrift extends BaseService implements FaceService {
  
  @Autowired
  private Cache faceCache;

  @Autowired
  private ThriftServiceClientProxyFactory imageService;

  @Autowired
  private FaceCommonService commonService;
  
  @Autowired
  private FdfsService fdfsService;

  //是否保存人脸照片
  @Value(value = "${face.image.save}")
  private boolean saveImage;

  //算法机参数
  private double[] scoreLevels;

  public double[] getScoreLevels() {
    return scoreLevels;
  }

  public void setScoreLevels(double[] scoreLevels) {
    this.scoreLevels = scoreLevels;
  }

  /**
   * 人脸检测
   */
  @Override
  public List<DetectedFace> detect(byte[] img, boolean attributes) throws FacePlatException {
    Iface client = (Iface) imageService.getObject();
    ImageData tr = new ImageData();
    tr.setImgdata(img);
    tr.setUse_face_features(true);
    tr.setUse_age_attribute(attributes);
    tr.setUse_gender_attribute(attributes);

    try {
      FaceInfo faceInfo = client.getFeatures(tr);
      List<FaceFeatures> faceFeatures = faceInfo.getFaceFeatures();
      List<DetectedFace> result = new ArrayList<>();
      for (FaceFeatures reIDFeature : faceFeatures) {
        DetectedFace detectedFace = new DetectedFace();
        detectedFace.setRotate(reIDFeature.getFacepicRotate());
        Rectangle rectangle = detectedFace.new Rectangle();
        //List<Integer> bbox = reIDFeature.getBbox();
        //assert bbox.size() == 4;
        rectangle.setLeft(reIDFeature.getFaceRectLeft());
        rectangle.setTop(reIDFeature.getFaceRectTop());
        rectangle.setWidth(reIDFeature.getFaceRectWidth());
        rectangle.setHeight(reIDFeature.getFaceRectHeight());
        detectedFace.setFaceRectangle(rectangle);

        List<Double> features = reIDFeature.getFeatures();
        float[] featureArray = new float[features.size()];
        for (int i = 0; i < features.size(); i++) {
          featureArray[i] = (float) features.get(i).doubleValue();
        }
        detectedFace.setFeature(featureArray);

        //人脸属性
        if (attributes) {
          int age = reIDFeature.getAge();
          double confidenceAge = reIDFeature.getConfidence_age();
          int gender = reIDFeature.getGender();
          double confidenceGender = reIDFeature.getConfidence_gender();
          FaceAttribute fa = new FaceAttribute();
          fa.setAge(age);
          fa.setAgeConfidence(confidenceAge);
          fa.setGender(gender);
          fa.setGenderConfidence(confidenceGender);
          detectedFace.setFaceAttribute(fa);
        }

        Face face = new Face();
        face.setToken(UUID.randomUUID().toString());
        face.setFeature(featureArray);
        //保存人脸照片
        if (saveImage) {
          BufferedImage bi;
          try {
            bi = ImageUtils.getBufferedImage(img);
            BufferedImage subimage = bi.getSubimage(rectangle.getLeft(), rectangle.getTop(), rectangle.getWidth(),rectangle.getHeight());
            //save in FastDFS
            face.setImgId(fdfsService.uploadToFastDFS(face.getToken() + "_base.jpg", ImageUtils.writeImageToBytes(subimage, "jpg")));
            face.setImgUrl(fdfsService.getFileUrl(face.getImgId()));
          } catch (IOException | InterruptedException | ExecutionException e) {
            log.error("process face image error", e);
          }
          //This image format (Jpeg-Custom) cannot be written.
          //face.setImg(Imaging.writeImageToBytes(subimage, ImageFormats.PNG, null)); 
          //face.setImg(ImageUtils.writeImageToBytes(subimage, "png"));
        }

        //保存人脸特征值
        commonService.saveFeature(face);
        //保存人脸基础信息
        commonService.saveFace(face);
        //存入ehcache
        faceCache.putIfAbsent(new Element(face.getToken(), face));
        detectedFace.setFaceToken(face.getToken());

        result.add(detectedFace);
      }
      return result;
    } catch (TException e) {
      log.error("face algorithm error", e);
      throw new FacePlatException(ErrorType.FACE_ALGO_ERROR, e);
    } 
  }

  /**
   * 人脸检测
   */
  @Override
  public List<DetectedFace> detect(byte[] img) throws FacePlatException {
    return detect(img, false);
  }

  /**
   * 获取人脸属性
   */
  @Override
  public List<DetectedFace> getFaceAttribute(byte[] img) throws FacePlatException {
    Iface client = (Iface) imageService.getObject();
    ImageData tr = new ImageData();
    tr.setImgdata(img);
    tr.setUse_face_features(false);
    tr.setUse_age_attribute(true);
    tr.setUse_gender_attribute(true);

    try {
      FaceInfo faceInfo = client.getFeatures(tr);

      List<DetectedFace> faList = new ArrayList<>();
      List<FaceFeatures> faceFeatures = faceInfo.getFaceFeatures();
      for (FaceFeatures reIDFeature : faceFeatures) {
        DetectedFace detectedFace = new DetectedFace();
        detectedFace.setRotate(reIDFeature.getFacepicRotate());
        Rectangle rectangle = detectedFace.new Rectangle();
        rectangle.setLeft(reIDFeature.getFaceRectLeft());
        rectangle.setTop(reIDFeature.getFaceRectTop());
        rectangle.setWidth(reIDFeature.getFaceRectWidth());
        rectangle.setHeight(reIDFeature.getFaceRectHeight());
        detectedFace.setFaceRectangle(rectangle);
        int age = reIDFeature.getAge();
        double confidenceAge = reIDFeature.getConfidence_age();
        int gender = reIDFeature.getGender();
        double confidenceGender = reIDFeature.getConfidence_gender();
        FaceAttribute fa = new FaceAttribute();
        fa.setAge(age);
        fa.setAgeConfidence(confidenceAge);
        fa.setGender(gender);
        fa.setGenderConfidence(confidenceGender);
        detectedFace.setFaceAttribute(fa);
        faList.add(detectedFace);
      }
      return faList;
    } catch (TException e) {
      log.error("face algorithm error", e);
      throw new FacePlatException(ErrorType.FACE_ALGO_ERROR, e);
    }
  }

  /**
   * 人脸比对
   */
  @Override
  public CompareResult compare(String faceToken1, String faceToken2, byte[] img1, byte[] img2)
      throws FacePlatException {
    Assert.isTrue(faceToken1 != null || img1 != null, "facetoken1 and img1 cannot be empty at the same time");
    Assert.isTrue(faceToken2 != null || img2 != null, "facetoken2 and img2 cannot be empty at the same time");

    CompareResult result = new CompareResult();

    float[] feature1;  
    if(!StringUtils.isEmpty(faceToken1)){
      Face face1 = commonService.findOneFace(faceToken1);
      if(face1 == null){
        throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
      }
      feature1 = face1.getFeature();
    }else{
      List<DetectedFace> detectedFaceList = detect(img1);
      if(detectedFaceList.isEmpty()){
        throw new FacePlatException(ErrorType.NO_FACE_DETECTED);
      }
      result.setFaces1(detectedFaceList);
      DetectedFace detectedFace = detectedFaceList.get(0);
      feature1 = detectedFace.getFeature();
    }

    float[] feature2;  
    if(!StringUtils.isEmpty(faceToken2)){
      Face face2 = commonService.findOneFace(faceToken2);
      if(face2 == null){
        throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
      }
      feature2 = face2.getFeature();
    }else{
      List<DetectedFace> detectedFaceList = detect(img2);
      if(detectedFaceList.isEmpty()){
        throw new FacePlatException(ErrorType.NO_FACE_DETECTED);
      }
      result.setFaces2(detectedFaceList);
      DetectedFace detectedFace = detectedFaceList.get(0);
      feature2 = detectedFace.getFeature();
    }

    result.setConfidence(getScore(calDist(feature1, feature2)));
    return result;
  }

  /**
   * 人脸搜索
   */
  @Override
  public SearchResult search(SearchParam param) throws FacePlatException {
    SearchResult searchResult = new SearchResult();
    String faceToken = null;
    float[] feature = {};
    if(!StringUtils.isEmpty(param.getFaceFeature())){
      feature = FeatureUtils.decode(param.getFaceFeature());
      faceToken = param.getFaceToken();
    }else if (!StringUtils.isEmpty(param.getFaceToken())) {
      Face face = commonService.findOneFace(param.getFaceToken());
      if(face == null){
        throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
      }
      feature = face.getFeature();
    }else if(param.getImageFile() != null){
      try{
        List<DetectedFace> detectedFaceList = detect(param.getImageFile().getBytes());
        searchResult.setFaces(detectedFaceList);
        if(detectedFaceList.isEmpty()){
          throw new FacePlatException(ErrorType.NO_FACE_DETECTED);
        }
        // 第一个人脸进行人脸搜索
        DetectedFace detectedFace = detectedFaceList.get(0);
        feature = detectedFace.getFeature();
        faceToken = detectedFace.getFaceToken();
      }catch(IOException e){
        log.error("read image failed", e);
        throw new FacePlatException(ErrorType.ARGUMENT_ERROR, e);
      }
    }else{
      throw new FacePlatException(ErrorType.ARGUMENT_ERROR);
    }
    return search_new(faceToken, feature, param.getFacesetToken(), param.getResultCount());
  }

  /**
   * Description: 从人脸库中检索是否存在<br>
   *
   * @author guming <br>
   * @taskId
   * @param param <br>
   * @return <br>
   */
  @Override
  public SearchOneResult searchOne(SearchsParam param) throws FacePlatException {
    SearchOneResult searchResult = new SearchOneResult();
    for(FaceSet faceSet : param.getFaceSets()){
      Map<String,float[]> features = commonService.getAllFeaturesOfFaceSet(faceSet.getFacesetToken());
      for(Entry<String, float[]> entry: features.entrySet()){
        float[] f = entry.getValue();
        double dist = calDist(param.getFaceFeature(), f);
        double score = getScore(dist);
        if(score * 100 >= faceSet.getThreshold()){
          searchResult.setExists(true);
          searchResult.setFaceSetToken(faceSet.getFacesetToken());
          searchResult.setFaceToken(entry.getKey());
          searchResult.setConfidence(score * 100);
          searchResult.setThreshold(faceSet.getThreshold());
          faceCache.putIfAbsent(new Element(param.getFeature(),searchResult));
          return searchResult;
        }
      }
    }
    return searchResult;
  }

  private SearchResult search_new(String faceToken, float[] feature, String facesetToken, int count) throws FacePlatException {
    SearchResult searchResult = new SearchResult();
    Map<String, float[]> features = commonService.getAllFeaturesOfFaceSet(facesetToken);
    List<Result> tempResults = new LinkedList<>();
    for(Entry<String, float[]> entry: features.entrySet()){
      float[] f = entry.getValue();
      double dist = calDist(feature, f);
      double score = getScore(dist);
      Result result = searchResult.new Result();
      result.setFaceToken(entry.getKey());
      result.setConfidence(score);
      tempResults.add(result);
    }
    List<Result> collect = tempResults.stream().sorted((e1, e2) -> (e1.getConfidence() < e2.getConfidence() ? 1 : -1)).limit(count)
            .collect(Collectors.toList());
    searchResult.setResults(collect);
    searchResult.setFaceToken(faceToken);
    return searchResult;
  }

  /*private SearchResult search(String faceToken, float[] feature, String facesetToken, int count) throws FacePlatException {
    SearchResult searchResult = new SearchResult();
    Map<String, float[]> features = commonService.getAllFeaturesOfFaceSet(facesetToken);
    List<Result> tempResults = new LinkedList<>();
    for(Entry<String, float[]> entry: features.entrySet()){
      float[] f = entry.getValue();
      double dist = calDist(feature, f);
      double score = getScore(dist);
      Result result = searchResult.new Result();
      result.setFaceToken(entry.getKey());
      result.setConfidence(score);
      if(tempResults.size() < count){
        tempResults.add(result);
      }else if(score > tempResults.get(count-1).getConfidence()){
        tempResults.set(count-1, result);
      }
      tempResults.sort((e1, e2) -> (e1.getConfidence() < e2.getConfidence() ? 1 : -1));
    }
    List<Result> collect = tempResults.stream().sorted((e1, e2) -> (e1.getConfidence() < e2.getConfidence() ? 1 : -1)).limit(count)
        .collect(Collectors.toList());
    searchResult.setResults(collect);
    searchResult.setFaceToken(faceToken);
    return searchResult;
  }*/
  


  // 计算N维向量的欧式距离的平方
  private strictfp double calDist(float[] faceFutureArray1, float[] faceFutureArray2) {
    Assert.isTrue(faceFutureArray1.length == faceFutureArray2.length, "feature length different, maybe face algorithmic model had changed");
    double dist = 0;
    for (int i = 0; i < faceFutureArray1.length; i++) {
      double faceFuture1 = faceFutureArray1[i];
      double faceFuture2 = faceFutureArray2[i];
      dist += Math.pow(faceFuture1 - faceFuture2, 2);
    }
    //return Math.sqrt(confidence);
    return dist;
  }

  //scoreLevels[]  置信度：万一、千一、百一  ，和算法模型配套。
  private strictfp double getScore(double dist) {
    float score = 0;
    if(dist < scoreLevels[0] * 0.333){
      score = 1;
    } else if (dist < scoreLevels[0]) {
      score = (float) (1.5*(scoreLevels[0] - dist) / scoreLevels[0] * 0.2 + 0.8);
    } else if ((dist >= scoreLevels[0]) && (dist <= scoreLevels[1])) {
      score = (float) (0.8 - (dist - scoreLevels[0]) / ((scoreLevels[1] - scoreLevels[0])) * 0.2);
    } else if ((dist > scoreLevels[1]) && (dist < scoreLevels[2])) {
      score = (float) ((scoreLevels[2] - dist) / (scoreLevels[2] - scoreLevels[1]) * 0.6);
    } else if (dist > scoreLevels[2]) {
      score = 0;
    }
    return (score <= 1.0 ? score : 1.0);
  }

  /**
   * 获取人脸特征值
   */
  @Override
  public float[] getFeatures(byte[] img) throws FacePlatException {
    Iface client = (Iface) imageService.getObject();
    ImageData tr = new ImageData();
    tr.setImgdata(img);
    tr.setUse_face_features(true);

    float[] featureArray = {};
    try {
      FaceInfo faceInfo = client.getFeatures(tr);
      List<FaceFeatures> faceFeatures = faceInfo.getFaceFeatures();
      if (CollectionUtils.isEmpty(faceFeatures)) {
        return featureArray;
      }
      
      List<Double> features = faceFeatures.get(0).getFeatures();
      int size = features.size();
      featureArray = new float[size];
      for (int i = 0; i < size; i++) {
        featureArray[i] = (float) features.get(i).doubleValue();
      }
      return featureArray;
    } catch (TException e) {
      log.error("face algorithm error", e);
      throw new FacePlatException(ErrorType.FACE_ALGO_ERROR, e);
    }
  }
  
  /**
   * 人脸检测，返回一个主要的人脸
   */
  @Override
  public DetectedFace detectOneFace(byte[] img) throws FacePlatException {
    Iface client = (Iface) imageService.getObject();
    ImageData tr = new ImageData();
    tr.setImgdata(img);
    tr.setUse_face_features(true);
    tr.setUse_age_attribute(false);
    tr.setUse_gender_attribute(false);

    try {
      FaceInfo faceInfo = client.getFeatures(tr);
      List<FaceFeatures> faceFeatures = faceInfo.getFaceFeatures();
      FaceFeatures reIDFeature = faceFeatures.get(0);
      DetectedFace detectedFace = new DetectedFace();
      
      Rectangle rectangle = detectedFace.new Rectangle();
      rectangle.setLeft(reIDFeature.getFaceRectLeft());
      rectangle.setTop(reIDFeature.getFaceRectTop());
      rectangle.setWidth(reIDFeature.getFaceRectWidth());
      rectangle.setHeight(reIDFeature.getFaceRectHeight());
      detectedFace.setFaceRectangle(rectangle);
      
      List<Double> features = reIDFeature.getFeatures();
      int size = features.size();
      float[] featureArray = new float[size];
      for (int i = 0; i < size; i++) {
        featureArray[i] = features.get(i).floatValue();
      }
      detectedFace.setFeature(featureArray);

      return detectedFace;
    } catch (TException e) {
      log.error("face algorithm error", e);
      throw new FacePlatException(ErrorType.FACE_ALGO_ERROR, e);
    } 
  }

  /**
   * 保存人脸到缓存中，一级ehcache，二级redis
   */
  @Override
  public Face saveFace2Cache(MultipartFile img, DetectedFace detectedFace) throws FacePlatException {
    Face face = new Face();
    face.setToken(UUID.randomUUID().toString());
    face.setFeature(detectedFace.getFeature());

    //保存人脸照片
    if (saveImage) {
      BufferedImage bi;
      try {
        bi = ImageUtils.getBufferedImage(img.getBytes());
        BufferedImage subimage = bi.getSubimage(detectedFace.getFaceRectangle().getLeft(), detectedFace.getFaceRectangle().getTop(), detectedFace.getFaceRectangle().getWidth(), detectedFace.getFaceRectangle().getHeight());
        
        //save image
        byte[] imgBytes = ImageUtils.writeImageToBytes(subimage, "jpg");
        face.setImgId(fdfsService.uploadToFastDFS(face.getToken() + "_base.jpg", imgBytes));
        face.setImgUrl(fdfsService.getFileUrl(face.getImgId()));
      } catch (Exception e) {
        log.error("save face image failed:" + img.getOriginalFilename(), e);
      }
    }

    commonService.saveFace(face);
    faceCache.putIfAbsent(new Element(face.getToken(), face));
    return face;
  }

}
